package alpaca.core;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import alpaca.config.IConfigurator;
import alpaca.database.IDatabaseSystem;
import alpaca.network.IClientConnection;
import alpaca.network.NetworkMessage;
import alpaca.network.NetworkMessage.Type;
import alpaca.textprocessor.ITextProcessor;
import alpaca.util.BiDirMap;
import alpaca.util.DBConfigData;
import alpaca.util.SystemMessage;

import com.mysql.jdbc.exceptions.jdbc4.CommunicationsException;
import com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException;

public class ServerModel implements IServerModel {

	private static final String OFFLINE_NOTICE = "The user is offline, the message was stored and will be delivered upon his/her login";
	private static final String CONFIG_PROPERTIES = "config.properties";
	
	private PropertyChangeSupport pcs;
	private IClientConnection clientConn;
	private IDatabaseSystem databaseSystem;
	private ITextProcessor textProcessor;
	private IConfigurator configurator;
	private DBConfigData configData;

	/* Mapping of users connected to the server and their connection ID in the network subsystem*/
	private BiDirMap<IUser, Integer> onlineUsers = new BiDirMap<IUser, Integer>();

	public ServerModel(IClientConnection clientConn,
			IDatabaseSystem databaseSystem, ITextProcessor textProcessor, IConfigurator configurator) {

		this.clientConn = clientConn;
		this.databaseSystem = databaseSystem;
		this.textProcessor = textProcessor;
		this.configurator = configurator;
		
		this.pcs = new PropertyChangeSupport(this);
		this.clientConn.addPropertyChangeListener(this);
	}

	@Override
	public void init(){ 
		loadDBConfig();
	}

	@Override
	public void addPropertyChangeListener(PropertyChangeListener pcl) {
		pcs.addPropertyChangeListener(pcl);
	}

	@Override
	public void removePropertyChangeListener(PropertyChangeListener pcl) {
		pcs.removePropertyChangeListener(pcl);
	}

	@Override
	public void start(int port, DBConfigData configData){
		try {
			this.configData = configData;
			databaseSystem.createConnection(configData.getDb(), configData.getUser(), configData.getPassword());
			databaseSystem.createTables();
			clientConn.start(port);
			pcs.firePropertyChange("userlist", null, new SystemMessage(databaseSystem.getUserList(), SystemMessage.Type.UPDATE_USERS));
			pcs.firePropertyChange("start", null, new SystemMessage(true, SystemMessage.Type.SERV_STATUS));
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
			pcs.firePropertyChange("serverError", null, new SystemMessage(e.getMessage(), SystemMessage.Type.ERROR_MESSAGE));
			pcs.firePropertyChange("stop", null, new SystemMessage(false, SystemMessage.Type.SERV_STATUS));
		} catch (SQLException e) {
			e.printStackTrace();
			pcs.firePropertyChange("serverError", null, new SystemMessage(e.getMessage(), SystemMessage.Type.ERROR_MESSAGE));
			pcs.firePropertyChange("stop", null, new SystemMessage(false, SystemMessage.Type.SERV_STATUS));
		} catch (IOException e) {
			try {
				pcs.firePropertyChange("serverError", null, new SystemMessage(e.getMessage(), SystemMessage.Type.ERROR_MESSAGE));
				pcs.firePropertyChange("stop", null, new SystemMessage(false, SystemMessage.Type.SERV_STATUS));
				databaseSystem.closeConnection();
			} catch (SQLException e1) {
				e1.printStackTrace();
			}
			e.printStackTrace();
		}

		saveDBConfig(configData);
	}

	@Override
	public void stop(){
		try {
			databaseSystem.closeConnection();
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			clientConn.stop();
			pcs.firePropertyChange("stop", null, new SystemMessage(false, SystemMessage.Type.SERV_STATUS));
		}
	}

	@Override
	public void createUser(String firstName, String lastName, String email, String password) { 
		try {
			databaseSystem.createUser(firstName, lastName, email, password);
			pcs.firePropertyChange("userlist", null, new SystemMessage(databaseSystem.getUserList(), SystemMessage.Type.UPDATE_USERS));
		} catch (CommunicationsException e){
			try {
				databaseSystem.createConnection(configData.getDb(), configData.getUser(), configData.getPassword());
				createUser(firstName, lastName, email, password);
			} catch (SQLException e1) {
				clientConn.stop();
				e1.printStackTrace();
			}
		} catch (MySQLIntegrityConstraintViolationException e) {
			pcs.firePropertyChange("serverError", null, new SystemMessage("<html>User was not added <br>The e-mail address is already in the database</html>", SystemMessage.Type.ERROR_MESSAGE));
			e.printStackTrace();
		} catch (SQLException e) {
			pcs.firePropertyChange("serverError", null, new SystemMessage("<html>An error occured<br>The user was not added </html>", SystemMessage.Type.ERROR_MESSAGE));
			e.printStackTrace();
		}
	}

	@Override
	public void editUser(IUser user, String firstName, String lastName, String email, String password, boolean active){
		try {
			if ((!user.getEmail().equals(email) && onlineUsers.containsKey(user)) || !active && onlineUsers.containsKey(user)){
				clientConn.closeConnection(onlineUsers.getValue(user));
				onlineUsers.removeKey(user);
			}
			databaseSystem.updateUser(user, firstName, lastName, email, password, active);
			pcs.firePropertyChange("userlist", null, new SystemMessage(databaseSystem.getUserList(), SystemMessage.Type.UPDATE_USERS));
			pushContactListUpdate(user);
		} catch (MySQLIntegrityConstraintViolationException e) {
			pcs.firePropertyChange("serverError", null, new SystemMessage("<html>Change was not made <br>The e-mail address is already in the database</html>", SystemMessage.Type.ERROR_MESSAGE));
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void removeUser(IUser user) {
		try {
			if (onlineUsers.containsKey(user)){
				clientConn.closeConnection(onlineUsers.getValue(user));
				onlineUsers.removeKey(user);
			}
			databaseSystem.deleteUser(user);
			pcs.firePropertyChange("userlist", null, new SystemMessage(databaseSystem.getUserList(), SystemMessage.Type.UPDATE_USERS));
			pushContactListUpdate(user);

		} catch (CommunicationsException e){
			try {
				databaseSystem.createConnection(configData.getDb(), configData.getUser(), configData.getPassword());
				removeUser(user);
			} catch (SQLException e1) {
				clientConn.stop();
				e1.printStackTrace();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	
	@Override
	public void searchUserLogs(IUser fromUser, IUser toUser, Date fromDate) {
		try {
			MessageList ml = databaseSystem.getLogMessages(fromUser, toUser, fromDate, new Date(Calendar.getInstance().getTimeInMillis()));		
			pcs.firePropertyChange("messageLog", null, new SystemMessage(ml, SystemMessage.Type.MESSAGE_LOG));
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	/**
	 * Saves the database configurations to a file.
	 * @param configData DBConfigData
	 */
	private void saveDBConfig(DBConfigData configData) {
		Map<String,String> data = new HashMap<String,String>();
		if (configData.getDb() != null)
			data.put("db", configData.getDb());
		if (configData.getUser() != null)
			data.put("user", configData.getUser());
		if (configData.getPassword() != null)
			data.put("password", configData.getPassword());

		try {
			configurator.save(data, CONFIG_PROPERTIES);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Loads database configurations from a file.
	 */
	private void loadDBConfig() {
		try {
			Map<String,String> data = configurator.load(CONFIG_PROPERTIES);
			DBConfigData configData = new DBConfigData(
					(data.containsKey("db")) ? data.get("db") : null,
							(data.containsKey("user")) ? data.get("user") : null,
									(data.containsKey("password")) ? data.get("password") : null);

			pcs.firePropertyChange(null, null, new SystemMessage(configData, SystemMessage.Type.UPDATE_DB_CONFIG));
		} catch (IOException e) {
			System.err.println("Non-fatal error: database configuration file doesn't exist.");
		}
	}

	/**
	 * Processes a login attempt and sends a response back. It then sends an 
	 * updated contact list and then all offline messages that might be saved
	 * for the user in the database.
	 * @param nm NetworkMessage
	 */
	private void login(NetworkMessage nm) {
		LoginCredentials credentials = (LoginCredentials)nm.getObject();

		System.out.println("Login attempt: " + nm.getFromConnectionId() + " " + credentials.getEmail() + " " + credentials.getPassword());

		IUser activeUser = null;
		try {
			activeUser = databaseSystem.getUserByEmail(credentials.getEmail());
			//Check the password
			if (activeUser == null || !databaseSystem.checkPassword(activeUser, credentials.getPassword())) {
				clientConn.send(nm.getFromConnectionId(), new NetworkMessage("Email or password incorrect.", Type.LOGIN_FAIL));
				return;
			}
			
			//Checks if the user was online already, it then kicks the user out
			if (onlineUsers.containsKey(activeUser)){
				clientConn.closeConnection(onlineUsers.getValue(activeUser));
			}
			
			//Send login success and a contact list with updated status
			clientConn.send(nm.getFromConnectionId(), new NetworkMessage(activeUser, Type.LOGIN_SUCCESS));
			onlineUsers.put(activeUser, nm.getFromConnectionId());
			
			pcs.firePropertyChange("login", null, new SystemMessage(onlineUsers, SystemMessage.Type.UPDATE_CONN));
			pushStatusUpdate(activeUser);
			
			ContactList contactList = databaseSystem.getContactList(activeUser);
			setStatus(contactList);
			clientConn.send(nm.getFromConnectionId(), new NetworkMessage(contactList, Type.NEW_CONTACT_LIST));

			//Send offline messages
			MessageList messageList = databaseSystem.getOfflineMessages(activeUser);
			if (messageList != null){
				DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
				for (Message message : messageList) {
					clientConn.send(nm.getFromConnectionId(), new NetworkMessage(new Message("Message from " 
							+ dateFormat.format(message.getDate()) + ":", message.getFromUser(), message.getToUser(), new Date(Calendar.getInstance().getTimeInMillis())), Type.MESSAGE));
					clientConn.send(nm.getFromConnectionId(), new NetworkMessage(message, Type.MESSAGE));
				}
			}

		} catch (SQLException e) {
			e.printStackTrace();
			clientConn.send(nm.getFromConnectionId(), new NetworkMessage("Email or password incorrect.", Type.LOGIN_FAIL));
		}
	}

	/**
	 * Set online status for everyone in a contact list.
	 * @param contactList ContactList
	 */
	private void setStatus(ContactList contactList){
		for	(Contact c : contactList){
			c.getUser().setOnline(onlineUsers.containsKey(c.getUser()));
		}		
	}

	/**
	 * Searches for user in the database.
	 * @param nm NetworkMessage
	 */
	private void search(NetworkMessage nm) {
		SearchTerm searchTerm = (SearchTerm)nm.getObject();

		System.out.println("Search made: " + searchTerm);

		String[] terms = searchTerm.getTerm().split("\\s+");
		UserList userList = new UserList();

		for (int i = 0; i < terms.length; i++) {
			String term = terms[i];
			UserList tempList;

			try {
				tempList = databaseSystem.getUserList(term);
				userList.addAll(tempList);
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

		clientConn.send(nm.getFromConnectionId(), new NetworkMessage(userList, Type.SEARCH_RESULTS));
	}

	/**
	 * Add a contact to the contact list of the user that requested it.
	 * @param nm NetworkMessage
	 */
	private void addContact(NetworkMessage nm) {
		IUser user = (IUser) nm.getObject();
		Contact contact = new Contact(user, "Other contacts");

		IUser currentUser = null;
		ContactList contactList = null; 
		try {
			currentUser = databaseSystem.getUserByEmail(onlineUsers.getKey(nm.getFromConnectionId()).getEmail());
			databaseSystem.createContact(currentUser, contact);
			
			contactList = databaseSystem.getContactList(currentUser);
			setStatus(contactList);
			clientConn.send(nm.getFromConnectionId(), new NetworkMessage(contactList, Type.NEW_CONTACT_LIST));
			
			user.setOnline(onlineUsers.containsKey(user));
			clientConn.send(nm.getFromConnectionId(), new NetworkMessage(user, Type.STATUS_UPDATE));
		} catch (SQLException e) {
			e.printStackTrace();
		}

	}
	
	/**
	 * Add a contact to the contact list of the user that requested it.
	 * @param nm NetworkMessage
	 */
	private void removeContact(NetworkMessage nm) {
		IUser user = (IUser) nm.getObject();

		IUser currentUser;
		try {
			currentUser = databaseSystem.getUserByEmail(onlineUsers.getKey(nm.getFromConnectionId()).getEmail());
			databaseSystem.removeContact(currentUser, user);

			ContactList contactList = databaseSystem.getContactList(currentUser);
			setStatus(contactList);
			clientConn.send(nm.getFromConnectionId(), new NetworkMessage(contactList, Type.NEW_CONTACT_LIST));
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	/**
	 * Updates the tag for the contact in the contact list of the user that sent the request
	 * @param nm NetworkMessage
	 */
	private void assignTag(NetworkMessage nm) {
		Contact contact = (Contact) nm.getObject();
		IUser user;
		try {
			user = databaseSystem.getUserByEmail(onlineUsers.getKey(nm.getFromConnectionId()).getEmail());
			databaseSystem.updateTag(user, contact);

			ContactList contactList = databaseSystem.getContactList(user);
			setStatus(contactList);
			clientConn.send(nm.getFromConnectionId(), new NetworkMessage(contactList, Type.NEW_CONTACT_LIST));

		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Pushes out a new contact list for users having the user in the parameter.
	 * Used when more than the online-status has been changed.
	 * @param user IUser
	 */
	private void pushContactListUpdate(IUser user){
		System.out.println("Contact List Update!");
		try {
			UserList users = databaseSystem.getUsersWithContact(user);
			for (IUser cUser : users) {
				if (onlineUsers.containsKey(cUser)){
					ContactList contactList = databaseSystem.getContactList(cUser);
					setStatus(contactList);
					clientConn.send(onlineUsers.getValue(cUser), new NetworkMessage(contactList, Type.NEW_CONTACT_LIST));
				}
			}

		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Pushes out a new online status to every user having the user in the parameter as a contact.
	 * @param user IUser
	 */
	private void pushStatusUpdate(IUser user){
		System.out.println("User Update!");
		try {
			user.setOnline(onlineUsers.containsKey(user));
			NetworkMessage nm = new NetworkMessage(user, Type.STATUS_UPDATE);

			UserList users = databaseSystem.getUsersWithContact(user);
			for (IUser cUser : users) {
				if (onlineUsers.containsKey(cUser)){
					clientConn.send(onlineUsers.getValue(cUser), nm);
				}
			}

		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Handles incoming messages.
	 * @param nm NetworkMessage
	 */
	private void processMessage(NetworkMessage nm){
		Message message = (Message) nm.getObject();
		Message m = new Message(textProcessor.modifyText(message.getMessage()), message.getFromUser(), message.getToUser(), message.getDate());
		nm = new NetworkMessage(m, Type.MESSAGE);

		if(onlineUsers.containsKey(m.getToUser())){
			int connID = onlineUsers.getValue(m.getToUser());
			clientConn.send(connID, nm);
			connID = onlineUsers.getValue(m.getFromUser());
			clientConn.send(connID, nm);
			m.setSent(true);
		} else {
			Message msg = new Message(OFFLINE_NOTICE, m.getFromUser(), m.getToUser(), m.getDate());
			NetworkMessage nmsg = new NetworkMessage(msg, NetworkMessage.Type.MESSAGE);
			int connID = onlineUsers.getValue(m.getFromUser());
			clientConn.send(connID, nm);
			clientConn.send(connID, nmsg);
		}
		try {
			databaseSystem.addLogMessage(m);
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		if (evt.getNewValue() instanceof NetworkMessage){
			NetworkMessage nm = (NetworkMessage) evt.getNewValue();
			
			if (nm.getType().equals(Type.LOGIN)){
				login(nm);
			}else if (onlineUsers.containsValue(nm.getFromConnectionId())){
				switch(nm.getType()){

				case MESSAGE: 
					processMessage(nm);
					break;

				case SEARCH_TERM: 
					search(nm);
					break;

				case ADD_CONTACT: 
					addContact(nm);
					break;

				case ASSIGN_TAG:
					assignTag(nm);
					break;

				case REMOVE_CONTACT:
					removeContact(nm);
					break;
				}		
			} else {
				clientConn.closeConnection(nm.getFromConnectionId());
			}
			
		}else if (evt.getNewValue() instanceof SystemMessage){
			SystemMessage sm = (SystemMessage) evt.getNewValue();
			switch(sm.getType()){

			case DISCONNECT:
				Integer connID = (Integer) sm.getObject();
				if (onlineUsers.containsValue(connID)){
					IUser disconnectedUser = onlineUsers.getKey(connID);
					onlineUsers.removeValue(connID);
					pushStatusUpdate(disconnectedUser);	
				}
				pcs.firePropertyChange("disconnect", null, new SystemMessage(onlineUsers, SystemMessage.Type.UPDATE_CONN));
				break;

			case SERV_STATUS:
				pcs.firePropertyChange(evt);
				break;
			}
		}
	}

}
